package cn.miju.ser.wechatpay.serivce.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.miju.common.content.CodeMessage;
import cn.miju.common.util.ValidationUtil;
import cn.miju.database.constant.DbConstant;
import cn.miju.database.entity.PayOrder;
import cn.miju.database.service.MerchantDataService;
import cn.miju.database.service.PayOrderDataService;
import cn.miju.ser.common.rocketmq.DelayLevel;
import cn.miju.ser.common.rocketmq.Topic;
import cn.miju.ser.common.service.CommonService;
import cn.miju.ser.framework.constant.SerConstant;
import cn.miju.ser.framework.exception.ApiException;
import cn.miju.ser.framework.handler.MiJuContext;
import cn.miju.ser.framework.handler.MiJuContextHandler;
import cn.miju.ser.wechatpay.serivce.WechatpayService;
import cn.miju.ser.wechatpay.vo.*;
import cn.miju.wechatpay.handler.WechatpayApplyQrCodeHandler;
import cn.miju.wechatpay.handler.WechatpayCloseOrderHandler;
import cn.miju.wechatpay.handler.WechatpayQueryOrderHandler;
import cn.miju.wechatpay.handler.WechatpayRefundOrderHandler;
import cn.miju.wechatpay.request.WechatpayCloseOrderReq;
import cn.miju.wechatpay.request.WechatpayCreateQrcodeOrderReq;
import cn.miju.wechatpay.request.WechatpayQueryOrderReq;
import cn.miju.wechatpay.request.WechatpayRefundOrderReq;
import cn.miju.wechatpay.response.WechatpayCloseOrderResp;
import cn.miju.wechatpay.response.WechatpayCreateQrcodeOrderResp;
import cn.miju.wechatpay.response.WechatpayQueryOrderResp;
import cn.miju.wechatpay.response.WechatpayRefundOrderResp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * <pre>
 *
 * 文件名 :  WechatPayServiceImpl
 * 创建时间 : 2023年03月16日 11:05
 * 文件描述 : 无
 *
 * </pre>
 *
 * @author Huzb
 * @version 1.0.0
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatPayServiceImpl implements WechatpayService {

    private final CommonService commonService;
    private final RocketMQTemplate rocketMQTemplate;
    private final StringRedisTemplate redisTemplate;
    private final PayOrderDataService payOrderDataService;
    private final WechatpayApplyQrCodeHandler applyQrCodeHandler;
    private final WechatpayQueryOrderHandler queryOrderHandler;
    private final WechatpayCloseOrderHandler closeOrderHandler;
    private final WechatpayRefundOrderHandler refundOrderHandler;

    /**
     * 回调通知地址
     */
    @Value("${miju.wechatpay-pay-notify-url:}")
    private String notifyUrl;

    /**
     * 订单超时消息队列延迟等级
     */
    @Value("${miju.mq-delay-level}")
    private DelayLevel delayLevel;

    /**
     * 扫码支付下单
     *
     * @param inVO 入参
     * @return 返回结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CreateQrcodeOrderOutVO createQrcodeOrder(CreateQrcodeOrderInVO inVO) {
        ValidationUtil.validate(inVO);
        MiJuContext miJuContext = MiJuContextHandler.getMiJuContext();

        // 先看商户订单号是否存在, 这里直接getOne就好了，如果出现数据库错误一定是系统错误+数据库错误
        PayOrder payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>()
                .eq(PayOrder::getMerchantId, miJuContext.getMerchantId())
                .eq(PayOrder::getOutTradeNo, inVO.getOutTradeNo()));

        // 针对重复下单二维码复用
        if (Objects.nonNull(payOrder) && Boolean.TRUE.equals(redisTemplate.hasKey(SerConstant.Redis.SCAN_CODE_PAY_QRCODE + payOrder.getPayWay() + ":" + payOrder.getPlatTradeNo()))) {
            String qrcode = redisTemplate.opsForValue().get(SerConstant.Redis.SCAN_CODE_PAY_QRCODE + payOrder.getPayWay() + ":" + payOrder.getPlatTradeNo());
            return new CreateQrcodeOrderOutVO().setQrcode(qrcode);
        }

        payOrder = PayOrder.builder()
                .merchantId(miJuContext.getMerchantId())
                .outTradeNo(inVO.getOutTradeNo())
                .platTradeNo(commonService.getPlatTradeNo())
                .createdOrderTime(LocalDateTime.now())
                .payAmount(inVO.getTotalAmount())
                .payScen(DbConstant.PayScen.SCAN_CODE)
                .payWay(DbConstant.PayWay.WECHATPAY)
                .status(DbConstant.PayOrderStatus.WAITING).build();
        payOrderDataService.save(payOrder);

        // 转换过期时间
        String timeExpireStr = getTimeExpireStr(inVO.getTimeExpire());
        WechatpayCreateQrcodeOrderResp resp = applyQrCodeHandler.doPlan(new WechatpayCreateQrcodeOrderReq()
                .setOutTradeNo(payOrder.getPlatTradeNo())
                .setDescription(inVO.getDescription())
                .setTimeExpire(timeExpireStr)
                .setNotifyUrl(notifyUrl)
                .setAmount(inVO.getTotalAmount()));
        if (!resp.isSuccess()) {
            throw new ApiException(CodeMessage.FAIL.getCode(), resp.getMsg());
        }

        // 返回，在订单有效期内存储redis，便于复用
        redisTemplate.opsForValue().set(SerConstant.Redis.SCAN_CODE_PAY_QRCODE + payOrder.getPayWay() + ":" + payOrder.getPlatTradeNo(),
                resp.getQrcode(), getRemainingTimeFromEndTime(timeExpireStr), TimeUnit.SECONDS);
        rocketMQTemplate.syncSend(Topic.TIMEOUT_PAY_ORDER_TOPIC, MessageBuilder.withPayload(payOrder.getId()).build(), 3000, delayLevel.getLevel());
        return new CreateQrcodeOrderOutVO().setQrcode(resp.getQrcode());
    }

    /**
     * 订单查询
     *
     * @param inVO 入参
     * @return 返回结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public QueryOrderOutVO queryOrder(QueryOrderInVO inVO) {
        if (StringUtils.isBlank(inVO.getOutTradeNo()) && StringUtils.isBlank(inVO.getPlatTradeNo())) {
            throw new ApiException(CodeMessage.VALIDATE_FAIL.getCode(), "传入商户订单号和平台订单号不能都为空");
        }
        PayOrder payOrder;
        if (StrUtil.isNotBlank(inVO.getPlatTradeNo())) {
            payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>()
                    .eq(PayOrder::getPlatTradeNo, inVO.getPlatTradeNo()));
        } else {
            payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>()
                    .eq(PayOrder::getOutTradeNo, inVO.getOutTradeNo()));
        }
        if (BeanUtil.isEmpty(payOrder)) {
            throw new ApiException(CodeMessage.TRADE_NOT_EXIT);
        }

        // 只有未明确订单状态的订单才需要向渠道发起查询
        if (payOrder.getStatus().equals(DbConstant.PayOrderStatus.WAITING)) {
            WechatpayQueryOrderResp resp = queryOrderHandler.doPlan(new WechatpayQueryOrderReq()
                    .setOutTradeNo(payOrder.getPlatTradeNo()));
            if (resp.isSuccess()) {
                // 更新订单状态
                payOrder.setTradeNo(resp.getTransactionId());
                switch (resp.getTradeState()) {
                    case "SUCCESS":
                        // 支付成功
                        payOrder.setStatus(DbConstant.PayOrderStatus.SUCCESS);
                        payOrder.setPayOrderTime(LocalDateTime.parse(resp.getSuccessTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME));
                        payOrderDataService.updateById(payOrder);
                        break;
                    case "CLOSED":
                        // 已关闭
                    case "REVOKED":
                        // 已撤销
                        payOrder.setStatus(DbConstant.PayOrderStatus.CANCEL);
                        payOrderDataService.updateById(payOrder);
                        break;
                    case "NOTPAY":
                        // 未支付
                    case "USERPAYING":
                        break;
                    case "PAYERROR":
                        payOrder.setStatus(DbConstant.PayOrderStatus.FAIL);
                        payOrderDataService.updateById(payOrder);
                        break;
                    default:
                        throw new ApiException(CodeMessage.FAIL, "微信交易订单状态异常！");
                }
            } else {
                throw new ApiException(CodeMessage.FAIL, resp.getMsg());
            }
        }

        return new QueryOrderOutVO()
                .setOutTradeNo(payOrder.getOutTradeNo())
                .setPlatTradeNo(payOrder.getPlatTradeNo())
                .setTotalAmount(payOrder.getPayAmount())
                .setTradeStatus(payOrder.getStatus())
                .setPaymentTime(payOrder.getPayOrderTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }

    /**
     * 订单退款
     *
     * @param inVO 入参
     * @return 返回结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public RefundOrderOutVO refundOrder(RefundOrderInVO inVO) {
        if (StringUtils.isBlank(inVO.getOutTradeNo()) && StringUtils.isBlank(inVO.getPlatTradeNo())) {
            throw new ApiException(CodeMessage.VALIDATE_FAIL.getCode(), "传入商户订单号和平台订单号不能都为空");
        }
        PayOrder payOrder;
        if (StrUtil.isNotBlank(inVO.getPlatTradeNo())) {
            payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>()
                    .eq(PayOrder::getPlatTradeNo, inVO.getPlatTradeNo()));
        } else {
            payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>()
                    .eq(PayOrder::getOutTradeNo, inVO.getOutTradeNo()));
        }
        if (BeanUtil.isEmpty(payOrder)) {
            throw new ApiException(CodeMessage.TRADE_NOT_EXIT);
        }

        if (payOrder.getStatus().equals(DbConstant.PayOrderStatus.WAITING)) {
            throw new ApiException(CodeMessage.FAIL.getCode(), "订单正在支付中，不可退款！");
        }
        if (payOrder.getStatus().equals(DbConstant.PayOrderStatus.FAIL)) {
            throw new ApiException(CodeMessage.FAIL.getCode(), "订单支付失败，不可退款！");
        }
        if (payOrder.getStatus().equals(DbConstant.PayOrderStatus.CANCEL)) {
            throw new ApiException(CodeMessage.FAIL.getCode(), "订单已关闭，不可退款！");
        }
        if (payOrder.getStatus().equals(DbConstant.PayOrderStatus.ALL_REFUND)) {
            throw new ApiException(CodeMessage.FAIL.getCode(), "订单已全额退款！");
        }
        if (payOrder.getStatus().equals(DbConstant.PayOrderStatus.TIMEOUT)) {
            throw new ApiException(CodeMessage.FAIL.getCode(), "订单交易超时，不可退款！");
        }

        // 计算退款金额
        int ableRefundAmount = payOrder.getPayAmount() - (payOrder.getRefundedAmount() == null ? 0 : payOrder.getRefundedAmount());
        if (ableRefundAmount > inVO.getRefundFee()) {
            throw new ApiException(CodeMessage.FAIL, "订单当前剩余最大退费金额为(分)：+" + ableRefundAmount);
        }


        WechatpayRefundOrderResp resp = refundOrderHandler.doPlan(new WechatpayRefundOrderReq()
                .setOutTradeNo(payOrder.getPlatTradeNo())
                .setOutRefundNo(inVO.getOutRefundNo())
                .setNotifyUrl(inVO.getNotifyUrl())
                .setRefund(inVO.getRefundFee())
                .setTotal(payOrder.getPayAmount()));
        if (!resp.isSuccess()) {
            throw new ApiException(CodeMessage.FAIL.getCode(), resp.getMsg());
        }
        payOrder.setRefundedAmount(payOrder.getRefundedAmount() != null ?
                payOrder.getRefundedAmount() + inVO.getRefundFee() : inVO.getRefundFee());
        payOrder.setStatus(payOrder.getPayAmount() > payOrder.getRefundedAmount() ?
                DbConstant.PayOrderStatus.PART_REFUND : DbConstant.PayOrderStatus.ALL_REFUND);
        payOrderDataService.updateById(payOrder);

        return new RefundOrderOutVO();
    }

    /**
     * 订单关闭
     *
     * @param inVO 入参
     * @return 返回结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public CloseOrderOutVO closeOrder(CloseOrderInVO inVO) {
        if (StringUtils.isBlank(inVO.getOutTradeNo()) && StringUtils.isBlank(inVO.getPlatTradeNo())) {
            throw new ApiException(CodeMessage.VALIDATE_FAIL.getCode(), "传入商户订单号和平台订单号不能都为空");
        }
        PayOrder payOrder;
        if (StrUtil.isNotBlank(inVO.getPlatTradeNo())) {
            payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>()
                    .eq(PayOrder::getPlatTradeNo, inVO.getPlatTradeNo()));
        } else {
            payOrder = payOrderDataService.getOne(new LambdaQueryWrapper<PayOrder>()
                    .eq(PayOrder::getOutTradeNo, inVO.getOutTradeNo()));
        }
        if (BeanUtil.isEmpty(payOrder)) {
            throw new ApiException(CodeMessage.TRADE_NOT_EXIT);
        }

        // 只有待支付订单可以退款
        if (payOrder.getStatus().equals(DbConstant.PayOrderStatus.WAITING)) {

            WechatpayCloseOrderResp resp = closeOrderHandler.doPlan(new WechatpayCloseOrderReq()
                    .setOutTradeNo(payOrder.getPlatTradeNo()));
            if (!resp.isSuccess()) {
                throw new ApiException(CodeMessage.FAIL.getCode(), resp.getMsg());
            }
            payOrder.setStatus(DbConstant.PayOrderStatus.CANCEL);
            payOrderDataService.updateById(payOrder);
            return new CloseOrderOutVO();
        } else {
            throw new ApiException(CodeMessage.FAIL.getCode(), "订单已产生交易，不可关闭！");
        }
    }


    /**
     * 获取剩余时间
     *
     * @param endTimeStr 结束时间
     * @return 剩余时间
     */
    private static Long getRemainingTimeFromEndTime(String endTimeStr) {
        LocalDateTime endTime = LocalDateTime.parse(endTimeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        LocalDateTime now = LocalDateTime.now();
        Duration duration = Duration.between(now, endTime);
        long millis = duration.toMillis();
        if ((millis / 1000) > 0) {
            return millis / 1000;
        } else {
            // 这里返回1秒，如果传0 redis数据会变成永久有效 ttl为-1
            return 1L;
        }
    }

    /**
     * 获取过期时间
     *
     * @param second 秒
     * @return 到期时间
     */
    private static String getTimeExpireStr(Integer second) {
        if (second == null) {
            second = 7200;
        }
        return LocalDateTime.now().plusSeconds(second).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}
